Skip to content

fix(install): unwrap lspServers envelope in plugin .lsp.json (closes #1683)#1686

Merged
danielmeppiel merged 3 commits into
mainfrom
sergio-sisternes-epam/fix-lsp-json-unwrap-lspservers-envelope
Jun 8, 2026
Merged

fix(install): unwrap lspServers envelope in plugin .lsp.json (closes #1683)#1686
danielmeppiel merged 3 commits into
mainfrom
sergio-sisternes-epam/fix-lsp-json-unwrap-lspservers-envelope

Conversation

@sergio-sisternes-epam

Copy link
Copy Markdown
Collaborator

TL;DR

Plugin .lsp.json files using the { "lspServers": { ... } } wrapper format are now correctly unwrapped during install, instead of being silently skipped with a misleading validation error.

Problem

When a plugin ships a .lsp.json using the standard wrapper format:

{
  "lspServers": {
    "my-lsp-server": {
      "command": "my-lsp",
      "args": ["--stdio"],
      "extensionToLanguage": { ".ext": "mylang" }
    }
  }
}

_read_lsp_json() returned the parsed JSON verbatim, causing "lspServers" to be treated as a server name. Its nested dict value (containing the actual servers) was then treated as that server's config -- which lacks a command field, producing:

[!] Skipping invalid LSP server 'lspServers' from plugin 'my-plugin': LSP dependency 'lspServers' requires 'command'

The real LSP servers were never written to .github/lsp.json.

Fix

In _read_lsp_json() (src/apm_cli/deps/plugin_parser.py), after parsing the JSON dict, detect whether it contains a "lspServers" key with a dict value. If so, return the inner dict (the actual server map). The flat format (server names as top-level keys) continues to work unchanged.

This mirrors the envelope handling already used by LSPIntegrator._write_target_config() when writing to Copilot and Claude config files.

Changes

File Change
src/apm_cli/deps/plugin_parser.py 3-line envelope unwrap + updated docstring
tests/unit/deps/test_plugin_parser_lsp.py 4 new tests

Tests added

  • test_unwraps_lsp_servers_envelope -- wrapped format returns inner servers
  • test_flat_format_still_works -- flat format regression guard
  • test_auto_discovery_with_lsp_servers_wrapper -- auto-discovered .lsp.json with wrapper
  • test_wrapped_lsp_json_produces_valid_deps -- end-to-end: wrapped file -> valid deps

Validation

  • 20/20 tests pass in tests/unit/deps/test_plugin_parser_lsp.py
  • ruff check + ruff format --check clean
  • pylint R0801 (duplication) clean

Fixes #1683

When a plugin ships a .lsp.json using the { "lspServers": { ... } }
wrapper format, _read_lsp_json() now detects and unwraps the envelope
instead of treating "lspServers" as a server name.

This fixes the silent skip with:
  Skipping invalid LSP server 'lspServers': requires 'command'

The flat format (server names as top-level keys) continues to work
unchanged.

Fixes #1683

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 6, 2026 22:56
@sergio-sisternes-epam sergio-sisternes-epam added the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes apm install handling of plugin-provided .lsp.json files by supporting the common { "lspServers": { ... } } wrapper format, so wrapped LSP server definitions are correctly discovered and converted into dependencies instead of being misinterpreted and skipped.

Changes:

  • Update _read_lsp_json() to unwrap the "lspServers" envelope when present.
  • Expand unit tests to cover wrapped .lsp.json parsing, auto-discovery behavior, and end-to-end conversion into valid LSP dependencies.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/apm_cli/deps/plugin_parser.py Adds unwrap logic for the "lspServers" envelope and updates the docstring to document both supported formats.
tests/unit/deps/test_plugin_parser_lsp.py Adds new tests covering wrapped parsing, auto-discovery with wrapper, and end-to-end dependency generation.

Comment thread src/apm_cli/deps/plugin_parser.py
Comment thread tests/unit/deps/test_plugin_parser_lsp.py
Only unwrap the envelope when all values in the inner dict are dicts
(i.e. it looks like a server map). A flat-format server literally
named 'lspServers' would have scalar values like 'command', so the
all-dicts check avoids mis-detecting it as an envelope.

Adds a regression test for this ambiguous edge case.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses panel follow-ups for PR #1686 by proving wrapped plugin .lsp.json files produce dependencies.lsp entries without warning and documenting the accepted plugin file shapes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel removed the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 8, 2026
@danielmeppiel

Copy link
Copy Markdown
Collaborator

APM Review Panel: ship_now

Ship-ready compatibility fix: plugin .lsp.json envelope unwrap eliminates false warnings without breaking flat-form users.

cc @sergio-sisternes-epam @danielmeppiel -- a fresh advisory pass is ready for your review.

All specialist lenses converged: no correctness, security, or architecture concerns remain. The parser change is narrow, the docs now name both accepted plugin .lsp.json shapes, and the regression suite proves both the data path and the no-warning promise. Mutation-break confirmed the new tests fail if the unwrap guard is removed, then the guard was restored.

Aligned with: portable-by-manifest: accepts native Copilot/Claude LSP shape without a config rewrite; multi-harness: keeps Claude and Copilot LSP formats interoperable; pragmatic-as-npm: plugin authors can bring existing config as-is; oss-community-driven: community PR now has green CI and reviewer-ready evidence.

Growth signal. This is a zero-migration-tax compatibility fix worth calling out in release notes: existing Copilot/Claude .lsp.json envelopes now work as plugin input.

Panel summary

Persona B R N Takeaway
Python Architect 0 0 1 Clean, minimal change; unwrap heuristic distinguishes envelope from flat server named lspServers and tests cover core paths.
CLI Logging Expert 0 0 1 Clean CLI-output change; valid wrapper is silent by default and debug trace is appropriate for verbose troubleshooting.
DevX UX Expert 0 0 0 User-transparent compatibility fix; plugin authors can keep native Copilot/Claude LSP shape without warnings.
Supply Chain Security Expert 0 0 1 No supply-chain concerns; no new file I/O, path joins, credential access, or execution surface.
OSS Growth Hacker 0 0 1 Community-friendly compatibility fix; docs now mention both plugin .lsp.json shapes.
Doc Writer 0 0 2 Docs are accurate, concise, and consistent with the parser.
Test Coverage Expert 0 0 0 Regression-trap coverage is strong: 22/22 parser tests pass and new tests cover warning absence plus apm.yml deps synthesis.

B/R/N are signal counts, not gates. The maintainer ships.

Recommendation

Merge when ready. The follow-ups surfaced in the first panel pass were folded: warning-absence coverage, synthesized dependencies.lsp coverage, debug traceability, and concise docs/apm-guide notes. Remaining panel observations are optional nits and do not need tracking.

Folded in this run

  • (panel) Assert valid wrapped .lsp.json emits no WARNING-level records -- resolved in 096baa6.
  • (panel) Prove wrapped plugin .lsp.json is synthesized into dependencies.lsp -- resolved in 096baa6.
  • (panel) Add debug trace when the envelope is unwrapped -- resolved in 096baa6.
  • (panel) Document plugin .lsp.json accepts flat map or { "lspServers": { ... } } envelope in docs and apm-guide -- resolved in 096baa6.

Copilot signals reviewed

No Copilot inline comments were present after two fetch rounds. The bot's overview comment was reviewed; it raised no actionable inline findings.

Regression-trap evidence (mutation-break gate)

  • tests/unit/deps/test_plugin_parser_lsp.py::TestReadLspJson::test_unwraps_lsp_servers_envelope_without_warning -- deleted _read_lsp_json unwrap guard by forcing the condition false; test FAILED as expected; guard restored.
  • tests/unit/deps/test_plugin_parser_lsp.py::TestLspServersToApmDeps::test_wrapped_lsp_json_is_written_to_apm_yml_deps -- deleted _read_lsp_json unwrap guard by forcing the condition false; test FAILED as expected; guard restored.

Lint contract

uv run --extra dev ruff check src/ tests/ and uv run --extra dev ruff format --check src/ tests/ both passed before push. Additional local guards also passed: pylint R0801 and scripts/lint-auth-signals.sh.

CI

gh pr checks 1686 --repo microsoft/apm reports all checks successful: 14 successful, 1 skipped, 0 failing, 0 pending (after 0 CI fix iteration(s)).

Mergeability status

Captured from gh pr view 1686 --json mergeable,mergeStateStatus,statusCheckRollup immediately after the last push of this run.

PR head SHA CEO stance iters folds defers Copilot rounds CI mergeable mergeStateStatus notes
#1686 096baa6 ship_now 1 4 0 2 green MERGEABLE BLOCKED pending required review

Convergence

1 outer iteration; 2 Copilot round(s). Final panel verdict: ship_now.

Ready for maintainer review.


Full per-persona findings

Python Architect

  • [nit] Sibling metadata keys beside lspServers are ignored when the envelope is unwrapped at src/apm_cli/deps/plugin_parser.py:503.
    Not a correctness issue because common siblings such as $schema are metadata; parser returns the server map by design.

CLI Logging Expert

  • [nit] Debug message logs the full path at src/apm_cli/deps/plugin_parser.py:511.
    Verbose output may be noisier with absolute paths, but this is consistent with existing parser diagnostics.

DevX UX Expert

No findings.

Supply Chain Security Expert

  • [nit] Top-level shallow copy leaves inner config dicts shared at src/apm_cli/deps/plugin_parser.py:511.
    Not a security issue because callers do not mutate the parsed config; defensive deep copy would be optional future hardening.

OSS Growth Hacker

  • [nit] Docs could include a tiny copy-paste wrapped .lsp.json example at docs/src/content/docs/consumer/install-lsp-servers.md:168.
    The current concise sentence is enough for this bug-fix scope; avoid bloat unless a future docs pass expands examples.

Auth Expert -- inactive

PR touches plugin parser, LSP parser tests, and LSP docs only; no auth, token, credential, host-classification, or git/HTTP auth surface is affected.

Doc Writer

  • [nit] apm-guide sentence shifts from target-write behavior to plugin-ingestion behavior at packages/apm-guide/.apm/skills/apm-usage/dependencies.md:382.
    Wording is accurate and brief; optional phrasing can wait for a docs sweep.
  • [nit] Docs could say which runtime shape APM emits at docs/src/content/docs/consumer/install-lsp-servers.md:168.
    Non-essential here because output shapes are already covered by the runtime table.

Test Coverage Expert

No findings.

This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.

@danielmeppiel danielmeppiel merged commit 2a5d5ba into main Jun 8, 2026
16 checks passed
@danielmeppiel danielmeppiel deleted the sergio-sisternes-epam/fix-lsp-json-unwrap-lspservers-envelope branch June 8, 2026 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Plugin .lsp.json with lspServers wrapper key is silently skipped during install

3 participants